package com.chinasoft.reposearch.service.impl;

import com.chinasoft.reposearch.entity.dto.SearchResultDTO;
import com.chinasoft.reposearch.entity.po.ElasticRequestParams;
import com.chinasoft.reposearch.entity.po.Repo;
import com.chinasoft.reposearch.service.ElasticService;
import com.chinasoft.reposearch.util.JacksonUtil;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.*;
import org.elasticsearch.client.indices.*;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@Service
public class ElasticServiceImpl implements ElasticService {
    private static final Logger logger = LoggerFactory.getLogger(ElasticServiceImpl.class);
    private final RestHighLevelClient client;

    @Autowired
    public ElasticServiceImpl(RestHighLevelClient client) {
        this.client = client;
    }

    /**
     * 创建索引
     *
     * @param resourcePath JSON配置文件的类路径
     * @param indexName 要创建的索引名称
     */
    @Override
    public void createIndex(String resourcePath, String indexName) {
        try {
            ClassPathResource resource = new ClassPathResource(resourcePath);
            if (!resource.exists()) {
                throw new IOException("JSON文件不存在:" + resourcePath);
            }

            if(ifExistsIndex(indexName)){
                return;
            }

            // 读取JSON文件内容
            String jsonContent = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);

            CreateIndexRequest request = new CreateIndexRequest(indexName);
            request.source(jsonContent, XContentType.JSON);

            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);

            if (response.isAcknowledged()) {
                logger.info("索引创建成功: {}", indexName);
            } else {
                logger.warn("索引创建未被确认: {}", indexName);
            }
        } catch (IOException e) {
            logger.error("读取JSON文件失败:{}", resourcePath, e);
            throw new RuntimeException(e);
        }catch (Exception e) {
            logger.error("创建索引失败:{}", resourcePath, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除指定名称的索引
     *
     * @param indexName 要删除的索引名称，不能为空
     */
    @Override
    public void deleteIndex(String indexName) {
        try {
            // 构建删除索引请求并执行删除操作
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
            if (delete.isAcknowledged()) {
                logger.info("索引删除成功:{}", indexName);
            } else {
                logger.warn("索引删除未被确认:{}", indexName);
            }
        } catch (IOException e) {
            logger.error("删除索引失败:{}", indexName, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 判断指定名称的索引是否存在
     *
     * @param indexName 索引名称
     * @return 如果索引存在返回true，否则返回false
     */
    @Override
    public Boolean ifExistsIndex(String indexName) {
        try {
            // 构造获取索引请求并判断索引是否存在
            GetIndexRequest request = new GetIndexRequest(indexName);
            boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
            if (exists) {
                logger.info("索引已存在:{}", indexName);
                return true;
            } else {
                logger.info("索引不存在:{}", indexName);
                return false;
            }
        } catch (IOException e) {
            logger.error("判断索引是否存在失败:{}", indexName, e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public void insertData(String indexName, String id, String data) {
        IndexRequest request = new IndexRequest(indexName).id(id);
        request.source(data,XContentType.JSON);
        try {
            client.index(request, RequestOptions.DEFAULT);
            logger.info("插入数据成功:{}", id);
        } catch (IOException e) {
            logger.error("插入数据失败:{}", id, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 批量插入数据到指定索引
     *
     * @param indexName 索引名称，指定数据要插入的目标索引
     * @param data 待插入的数据列表，每个元素为JSON格式的字符串
     */
    @Override
    public void batchInsertData(String indexName, List<String> data) {
        BulkRequest request = new BulkRequest();
        int i = 0;
        for (String item : data) {
            Repo repo = JacksonUtil.jsonToObject(item, Repo.class);
            String documentId = repo.getOwner() + "_" + repo.getName();
            request.add(new IndexRequest(indexName).id(documentId).source(item, XContentType.JSON));
            i++;

            if (i % 10000 == 0) {
                try {
                    logger.info("批量插入开始，第{}批", i / 10000);
                    BulkResponse bulk = client.bulk(request, RequestOptions.DEFAULT);
                    logger.info("批量插入完成");
                    if (bulk.hasFailures()) {
                        logger.error("批量插入数据失败:");
                    } else {
                        logger.info("批量插入数据成功:{}", bulk.getItems().length);
                    }
                } catch (IOException e) {
                    logger.error("批量插入异常:", e);
                    throw new RuntimeException(e);
                }
                // 关键：重置 request
                request = new BulkRequest();
            }
        }

        // 处理剩余不足1万条的数据
        if (request.numberOfActions() > 0) {
            try {
                logger.info("批量插入剩余数据: {}", request.numberOfActions());
                BulkResponse bulk = client.bulk(request, RequestOptions.DEFAULT);
                if (bulk.hasFailures()) {
                    logger.error("批量插入剩余数据失败");
                } else {
                    logger.info("批量插入剩余数据成功:{}", bulk.getItems().length);
                }
            } catch (IOException e) {
                logger.error("批量插入剩余数据异常:", e);
                throw new RuntimeException(e);
            }
        }
    }


    @Override
    public void updateData(String indexName, String id, String data) {
        UpdateRequest request = new UpdateRequest(indexName,id);
        request.doc(data,XContentType.JSON);
        try {
            client.update(request, RequestOptions.DEFAULT);
            logger.info("更新数据成功:{}", id);
        } catch (IOException e) {
            logger.error("更新数据失败:{}", id, e);
            throw new RuntimeException(e);
        }

    }

    @Override
    public void deleteData(String indexName, String id) {
        DeleteRequest request = new DeleteRequest(indexName,id);
        try {
            client.delete(request, RequestOptions.DEFAULT);
            logger.info("删除数据成功:{}", id);
        } catch (IOException e) {
            logger.error("删除数据失败:{}", id, e);
            throw new RuntimeException(e);
        }

    }

    @Override
    public String searchData(String indexName,String id) {
        GetRequest request = new GetRequest(indexName,id);
        try {
            GetResponse documentFields = client.get(request, RequestOptions.DEFAULT);
            if (!documentFields.isExists()) {
                return null;
            }
            logger.info("查询到数据:{}", documentFields.getId());
            return documentFields.getSourceAsString();
        } catch (IOException e) {
            logger.error("查询数据失败:{}", id, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 构建布尔查询条件并设置到搜索请求中
     *
     * @param params 包含查询参数的对象，用于构建查询条件
     * @param request Elasticsearch搜索请求对象，用于设置查询条件
     */
    private void buildBoolQuery(ElasticRequestParams params,SearchRequest  request){
        // 创建布尔查询构建器
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        // 如果语言参数不为空，则添加语言精确匹配查询条件
        if (params.getLanguage() != null && !params.getLanguage().isEmpty()) {
            boolQuery.must(QueryBuilders.termsQuery("primarylanguage", params.getLanguage()));
        }
        // 如果主题参数不为空，则添加主题精确匹配查询条件
        if (params.getTopic() != null && params.getTopic().length!=0) {
            boolQuery.must(QueryBuilders.nestedQuery("topics",
                    QueryBuilders.termsQuery("topics.name", params.getTopic()), ScoreMode.None));
        }
        // 将构建好的布尔查询设置到搜索请求中
        request.source().query(boolQuery);
    }


    /**
     * 根据Elasticsearch请求参数搜索数据列表
     *
     * @param params 包含搜索条件的参数对象，包括索引名称、关键字、字段、分页信息、排序字段和排序方式等
     * @return 搜索结果的字符串列表，每个元素为匹配文档的JSON字符串表示
     */
    @Override
    public SearchResultDTO searchDataList(ElasticRequestParams params) {
        // 构建搜索请求
        SearchRequest request = new SearchRequest(params.getIndexName());
        buildBoolQuery(params, request);
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        String sort = "";
        switch (params.getSort()) {
            case "star": sort = "stars";break;
            case "fork": sort = "forks";break;
            case "watcher": sort = "watchers";break;
            case "recent": sort = "pushedat";break;
            default : sort = "";
        }

        if (params.getKeyWord()==null||params.getKeyWord().isBlank()){
            request.source().query(QueryBuilders.matchAllQuery());
            boolQuery.minimumShouldMatch(1);
            request.source().query(boolQuery);
            request.source().sort(sort, SortOrder.valueOf(params.getOrder().toUpperCase()));
            request.source().from(params.getPage()).size(params.getSize());
        }else {
            boolQuery.should(QueryBuilders.multiMatchQuery(params.getKeyWord()).fields(params.getFieldWeights()).
                    type(MultiMatchQueryBuilder.Type.MOST_FIELDS));
            boolQuery.minimumShouldMatch(1);
            request.source().query(boolQuery);
            request.source().sort(sort, SortOrder.valueOf(params.getOrder().toUpperCase()));
            request.source().sort(SortBuilders.scoreSort().order(SortOrder.DESC));
            request.source().from(params.getPage()).size(params.getSize());
        }

        // 执行搜索并处理结果
        List<String> list = new ArrayList<>();
        try {
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);

            if (response.getShardFailures().length == 0) {
                logger.info("查询成功，总分片数:{}" , response.getTotalShards());
            }else {
                logger.warn("查询有部分失败，失败分片数:{}" , response.getFailedShards());
            }
            for (SearchHit hit : response.getHits().getHits()) {
                list.add(hit.getSourceAsString());
            }
            logger.info("查询到{}条数据", list.size());
            SearchResultDTO result = new SearchResultDTO();
            result.setTotal(response.getHits().getTotalHits().value);
            result.setData(list);
            return result;
        } catch (ElasticsearchException e) {
            // 客户端会在遇到严重错误时抛出异常
            if (e.status() == RestStatus.NOT_FOUND) {
                System.err.println("索引不存在");
            } else {
                System.err.println("Elasticsearch错误: " + e.getMessage());
            }
            return null;
        } catch (IOException e) {
            System.err.println("IO错误: " + e.getMessage());
            return null;
        }
    }
}
